# -*- coding: utf-8 -*-
__author__ = 'Jinkey'

import time

"""
在看完本教程之后你将学会:
1. 如何加载和准备数据
2. 如何创建一个基线神经网络模型
3. 如何使用scikit-learn 和 k-fold 交叉验证评估Keras模型
4. 数据准备如何提升你的模型的性能
5. 如何调整网络拓扑结构可以提高模型的性能实验

# 1 数据集描述
本教程中我们将使用的数据集是声纳数据集。

这是一个从多个服务收集回来描述声纳返回反射的数据集。60个输入变量描述了在不同角度的反射强度。这是一个二元分类问题,需要一个模型来区分岩石和金属圆筒。
你可以了解更多关于这个在UCI机器学习数据集库。你可以免费下载数据集,并将其重命名为sonar.csv放在您的工作目录。
这是一个很好理解的数据集。所有的变量是连续的,一般在0到1的范围。输出变量是一个字符串“M”和“R”我岩石,它将需要转化为整数 1 和 0 。
使用这个数据集的一个好处是,它是一个标准的基准问题。这意味着我们有一个好的模型的预期能力的想法。使用交叉验证,神经网络能够实现性能大约84%的上限为自定义模型精度在88%左右。

# 2 基准神经网络模型的表现
让我们来建立这个问题的基准模型和预测结果。
我们将从导入所有我们需要的类和函数开始:
"""

import numpy
import pandas
from keras.models import Sequential
from keras.layers import Dense
from keras.wrappers.scikit_learn import KerasClassifier
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline


"""
接下来,我们可以初始化随机数生成器,以确保执行这段代码时,我们总是得到相同的随机数序列。这有助于我们调试。
"""
seed = 7
numpy.random.seed(seed)

"""
现在我们可以使用 pandas 库来加载数据集。将列向量分割成60输入变量(X)和1个输出变量(Y)。我们使用 pandas 加载数据是因为它很容易处理字符串(输出变量),而试图使用 NumPy 加载数据会更困难。
"""
dataframe = pandas.read_csv("data/sonar.csv", header=None) # 加载数据集
dataset = dataframe.values
X = dataset[:, 0:60].astype(float) # 分割为60个输入变量
Y = dataset[:, 60] # 1个输出变量

"""
输出变量是字符串类型,我们必须将其转换为整数值 0 和 1。
我们可以使用从scikit-learn LabelEncoder类。这个类通过 fit() 函数获取整个数据集模型所需的编码,然后使用transform()函数应用编码来创建一个新的输出变量。
"""
encoder = LabelEncoder()
encoder.fit(Y)
encoded_Y = encoder.transform(Y)

"""
现在我们已经准备好使用Keras创建我们的神经网络模型。
我们将使用scikit-learn的k-fold交叉评估模型验证。这是一个重采样进行模型性能评估的技术。它通过把数据分成k个部分,训练模型在所有k部分中分出一个作为测试集对模型的性能进行评估。这个过程重复 k 次的平均分数作为所有构造模型稳健的性能评估。它是分层的,这意味着它将关注输出值并试图平衡K份数据中每个类别的势利数量。
在 scikit-learn 中使用 Keras 的模型,我们必须使用 KerasClassifier 进行包装。这个类起到创建并返回我们的神经网络模型的作用。它需要传入调用 fit()所需要的参数,比如迭代次数和批处理大小。
让我们开始定义一个函数用于创建我们的基准模型。我们的模型会有一个与输入层神经元相同数量（60个）的全连接隐藏层。这是一个建立神经网络时很好的默认起点。
权值初始化使用一个小的高斯随机数,激活函数使用 ReLU 函数。输出层包含单个神经元以作出预测。使用 sigmoid 激活函数是为了产生一个0到1的范围的概率,这可以很容易地和自动地转换为离散类型的值。
最后,我们使用对数损失函数(binary_crossentropy)训练,这是二元分类问题会优先使用的损失函数。模型还使用高效的 Adam 优化器来做梯度下降,精度指标将模在型训练时收集。

"""
# 基准模型
def create_baseline():
    # create model
    model = Sequential()
    model.add(Dense(60, input_dim=60, init='normal', activation='relu'))
    model.add(Dense(1, init='normal', activation='sigmoid'))
    # Compile model
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
    return model

"""
现在是时候来评估这个模型使用分层交叉验证scikit-learn框架。
我们使用 KerasClassifier 合理的默认值进行了数轮的迭代,关闭了10*10折交叉验证的详细过程在控制台输出。
"""
# evaluate model with standardized dataset
start_time = time.time()
estimator = KerasClassifier(build_fn=create_baseline, nb_epoch=100, batch_size=5, verbose=0)
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)
results = cross_val_score(estimator, X, encoded_Y, cv=kfold) # 如果get_parameters方法报错,查看 https://github.com/fchollet/keras/pull/5121/commits/01c6b7180116d80845a1a6dc1f3e0fe7ef0684d8
print("Baseline: %.2f%% (%.2f%%)" % (results.mean()*100, results.std()*100))
end_time = time.time()
print"用时: ", end_time - start_time
"""
运行这段代码输出了以下内容,显示了不可见数据集上的预测准确率和平均标准偏差。
> Baseline: 81.68% (5.67%)


# 3 带有数据准备地重新运行基准模型
建模之前准备数据是一个很好的练习。
神经网络模型特别适合有一致的输入值的情况,无论是在规模和分布。
有一种在训练神经网络模型中有效地准备一维数据的方法是“标准化”, 使每个属性的平均值为0，方差为1。这在保留高斯分布或类高斯分布的同时让数据向中心集中。
我们可以使用scikit-learn 的 StandardScaler 类来标准化声纳数据集.
我们应该在训练集而不是整个数据集上对数据进行标准化, 通过运行交叉验证和训练过的标准化数据去准备“未知”的训练数据。这让标准化在交叉验证中成为模型准备的一步, 也防止了算法在评估期间获得了数据准备过程对测试集的记忆。
我们可以利用 scikit-learn 的 Pipeline 类来实现这个过程。Pipeline 是一个包装器, 在交叉验证过程的执行一个或多个模型。在这里,我们可以定义一个Pipeline 带有一个tandardScaler, 后面紧跟着就是我们的神经网络模型。
"""


# evaluate baseline model with standardized dataset
start_time = time.time()
numpy.random.seed(seed)
estimators = []
estimators.append(('standardize', StandardScaler()))
estimators.append(('mlp', KerasClassifier(build_fn=create_baseline, nb_epoch=100, batch_size=5, verbose=0)))
pipeline = Pipeline(estimators)
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)
results = cross_val_score(pipeline, X, encoded_Y, cv=kfold)
print("Standardized: %.2f%% (%.2f%%)" % (results.mean()*100, results.std()*100))
end_time = time.time()
print"用时: ", end_time - start_time


"""
运行这段代码输出了以下内容,显示了不可见数据集上的预测准确率和平均标准偏差。
> Standardized: 84.07% (6.23%)


# 4 优化模型的层和神经元数量
有许多点可以优化神经网络,比如权重初始化,激活函数,优化器等等。
可能带来巨大影响的一个方面是网络本身的结构, 称为网络拓扑(the network topology)。在本节中,我们来看一看网络结构的两个实验:让它更小,让它大。
当在你的具体问题中优化神经网络,这些都是很好的试验方向。

## 4.2 评估一个更小型的网络
我怀疑在这个问题上有很多冗余的输入变量。
数据从不同的角度描述了相同的信号, 有些角度有可能比其他角度和信号更加相关。我们可以通过限制第一层的特征数量（输入层的神经元数量）来强制神经网络进行特征提取。
在这个实验中, 我们将基准模型的60个输入变量减少一半到30个。这会迫使神经网络从输入数据中提取出最重要的结构。
我们还会进行前面所说的数据标准化过程并利用减少规模带来的性能提升。

"""
# smaller model
def create_smaller():
	# create model
	model = Sequential()
	model.add(Dense(30, input_dim=60, init='normal', activation='relu'))
	model.add(Dense(1, init='normal', activation='sigmoid'))
	# Compile model
	model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
	return model

start_time = time.time()
numpy.random.seed(seed)
estimators = []
estimators.append(('standardize', StandardScaler()))
estimators.append(('mlp', KerasClassifier(build_fn=create_smaller, nb_epoch=100, batch_size=5, verbose=0)))
pipeline = Pipeline(estimators)
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)
results = cross_val_score(pipeline, X, encoded_Y, cv=kfold)
print("Smaller: %.2f%% (%.2f%%)" % (results.mean()*100, results.std()*100))
end_time = time.time()
print"用时: ", end_time - start_time

"""
运行这段代码输出了以下内容。我们可以看到在平均准确率上有了小幅度的提升,和标准误差的重要下降。
这是一个很好的结果,因为我们用了一半的规模却实现了准确率的提高,进而减少了一半的训练时间。

> Smaller: 84.61% (4.65%)

## 4.2 评估一个更大型的网络
有更多层的神经网络拓扑提供更多的机会来提取关键特征并把他们以非线性的方式联合起来。
我们可以评估是否可以简单地对模型做一些小的调整,添加更多层来提升神经网络的性能。这里我们在第一层隐藏层神经元后面再加一层30个神经元的隐藏层,我们的网络拓扑如下:
> 60 inputs -> [60 -> 30] -> 1 output

这里的想法是: 在变成瓶颈和被迫减半表征能力之前, 给更多机会去拟合所有的输入变量,非常像我们之前试验小型网络那样子。
不同于直接挤压输出的规模, 我们额外的添加一个隐藏层来实现这个过程。

"""
# larger model
def create_larger():
    # create model
    model = Sequential()
    model.add(Dense(60, input_dim=60, init='normal', activation='relu'))
    from keras.layers import Dropout
    model.add(Dropout(0.5))
    model.add(Dense(30, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(60, activation='relu'))
    model.add(Dense(1, activation='sigmoid'))
    # Compile model
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
    return model


start_time = time.time()
numpy.random.seed(seed)
estimators = []
estimators.append(('standardize', StandardScaler()))
estimators.append(('mlp', KerasClassifier(build_fn=create_larger, nb_epoch=100, batch_size=5, verbose=0)))
pipeline = Pipeline(estimators)
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=seed)
results = cross_val_score(pipeline, X, encoded_Y, cv=kfold)
print("Larger: %.2f%% (%.2f%%)" % (results.mean()*100, results.std()*100))
end_time = time.time()
print"用时: ", end_time - start_time

"""
运行这段代码输出了以下内容。我们看到了模型性能有个很给力的提升,用很小的努力就接近了完美的结果。
> Larger: 86.47% (3.82%)

进一步优化方面优化算法和训练时期的数量,预计进一步改进是可能的。这个数据集您可以实现最好的成绩是多少呢?

# 总结
在这篇文章中你学习了如何通过 Keras 循序渐进地解决二分类问题, 具体地来说:
1. 如何加载和准备数据
2. 如何创建一个基线神经网络模型
3. 如何使用scikit-learn 和 k-fold 交叉验证评估Keras模型
4. 数据准备如何提升你的模型的性能
5. 如何调整网络拓扑结构可以提高模型的性能实验



"""
